[PATCH 2/4] dnp3: reduce flood threshold to 32 and make configurable
authorJason Ish <jason.ish@oisf.net>
Tue, 6 Jan 2026 17:06:40 +0000 (11:06 -0600)
committerAndreas Dolp <dev@andreas-dolp.de>
Sun, 22 Feb 2026 12:28:52 +0000 (13:28 +0100)
Lower the number of unreplied requests from 500 to 32 to consider a
flood. At the very least this is an anomaly given the DNP3 spec mentions
that DNP3 should only have one outstanding request at a time, with an
exception for unsolicited responses, so in practice no more than 2
should be seen.

Additionally make this value configurable by introducing the max-tx
parameter.

Ticket: #8181
(cherry picked from commit a16f087b93be1ff2f2edf47371866ad9b28593c1)

Origin: upstream, https://github.com/OISF/suricata/commit/635af8dc8be09667689be71d781912718ca1aa49.patch
Bug: https://redmine.openinfosecfoundation.org/issues/8181
Subject: Upstream fix for CVE-2026-22259 part 2

Gbp-Pq: Name CVE-2026-22259_2.patch

doc/userguide/upgrade.rst
src/app-layer-dnp3.c
suricata.yaml.in

index 26df95ddf3fea0fc2d245fc7e15e9ea0bd4dc2ce..ed4e59c7b1450b454fee2098353f6396a59be234 100644 (file)
@@ -34,6 +34,16 @@ also check all the new features that have been added but are not covered by
 this guide. Those features are either not enabled by default or require
 dedicated new configuration.
 
+Upgrading to 7.0.14 (trixie-security 1:7.0.10-1~bpo13u3)
+-------------------
+
+Other Changes
+~~~~~~~~~~~~~
+- ``dnp3`` has reduced the default maximum number of outstanding
+  transactions from 500 down to 32. A ``max-tx`` parameter has been
+  added to the ``dnp3`` parser for users that need a larger number of
+  in-flight transactions.
+
 Upgrading to 7.0.9
 ------------------
 - The AF_PACKET default block size for both TPACKET_V2 and TPACKET_V3
index 4f210f036a505f10a84d97e7496f38c466d7223b..15de9bbe45ea5d0b4514e37bafe710611cbeef98 100644 (file)
@@ -40,9 +40,6 @@
 #include "app-layer-dnp3.h"
 #include "app-layer-dnp3-objects.h"
 
-/* Default number of unreplied requests to be considered a flood. */
-#define DNP3_DEFAULT_REQ_FLOOD_COUNT 500
-
 #define DNP3_DEFAULT_PORT "20000"
 
 /* Expected values for the start bytes. */
@@ -93,6 +90,14 @@ enum {
 /* Extract the range code from the object qualifier. */
 #define DNP3_OBJ_RANGE(x)  (x & 0xf)
 
+/* Default number of unreplied requests to be considered a flood.
+ *
+ * DNP3 is a request/response SCADA protocol with typically only 1-2
+ * transactions in flight. But set a limit high enough to allow for
+ * some pipelining but reduce the chance of memory exhaustion
+ * attacks. */
+static uint64_t dnp3_max_tx = 32;
+
 /* Decoder event map. */
 SCEnumCharMap dnp3_decoder_event_table[] = {
     {"FLOODED",           DNP3_DECODER_EVENT_FLOODED},
@@ -514,7 +519,7 @@ static DNP3Transaction *DNP3TxAlloc(DNP3State *dnp3, bool request)
     TAILQ_INSERT_TAIL(&dnp3->tx_list, tx, next);
 
     /* Check for flood state. */
-    if (dnp3->unreplied > DNP3_DEFAULT_REQ_FLOOD_COUNT) {
+    if (dnp3->unreplied > dnp3_max_tx && !dnp3->flooded) {
         DNP3SetEvent(dnp3, DNP3_DECODER_EVENT_FLOODED);
         dnp3->flooded = 1;
     }
@@ -1384,7 +1389,7 @@ static void DNP3StateTxFree(void *state, uint64_t tx_id)
         dnp3->unreplied--;
 
         /* Check flood state. */
-        if (dnp3->flooded && dnp3->unreplied < DNP3_DEFAULT_REQ_FLOOD_COUNT) {
+        if (dnp3->flooded && dnp3->unreplied < dnp3_max_tx) {
             dnp3->flooded = 0;
         }
 
@@ -1430,8 +1435,7 @@ static int DNP3GetAlstateProgress(void *tx, uint8_t direction)
     int retval = 0;
 
     /* If flooded, "ack" old transactions. */
-    if (dnp3->flooded && (dnp3->transaction_max -
-            dnp3tx->tx_num >= DNP3_DEFAULT_REQ_FLOOD_COUNT)) {
+    if (dnp3->flooded && (dnp3->transaction_max - dnp3tx->tx_num >= dnp3_max_tx)) {
         SCLogDebug("flooded: returning tx as done.");
         SCReturnInt(1);
     }
@@ -1604,8 +1608,13 @@ void RegisterDNP3Parsers(void)
         AppLayerParserRegisterTxDataFunc(IPPROTO_TCP, ALPROTO_DNP3,
             DNP3GetTxData);
         AppLayerParserRegisterStateDataFunc(IPPROTO_TCP, ALPROTO_DNP3, DNP3GetStateData);
-    }
-    else {
+
+        /* Parse max-tx configuration. */
+        intmax_t value = 0;
+        if (ConfGetInt("app-layer.protocols.dnp3.max-tx", &value)) {
+            dnp3_max_tx = (uint64_t)value;
+        }
+    } else {
         SCLogConfig("Parser disabled for protocol %s. "
             "Protocol detection still on.", proto_name);
     }
@@ -2252,7 +2261,7 @@ static int DNP3ParserTestFlooded(void)
     FAIL_IF_NOT(tx->done);
     FAIL_IF_NOT(DNP3GetAlstateProgress(tx, STREAM_TOSERVER));
 
-    for (int i = 0; i < DNP3_DEFAULT_REQ_FLOOD_COUNT - 1; i++) {
+    for (uint64_t i = 0; i < dnp3_max_tx - 1; i++) {
         SCMutexLock(&flow.m);
         FAIL_IF(AppLayerParserParse(NULL, alp_tctx, &flow, ALPROTO_DNP3,
                 STREAM_TOSERVER, request, sizeof(request)));
index b95d5e62f205eede1d28c36e4f542fac9055d414..1b54e1f24769759bb603bb068954f7ebf3448fa4 100644 (file)
@@ -1161,6 +1161,7 @@ app-layer:
       enabled: no
       detection-ports:
         dp: 20000
+      #max-tx: 32
 
     # SCADA EtherNet/IP and CIP protocol support
     enip: